#!/usr/bin/env python

import os.path

from pyfileutils import create_folder, rmtree
from pyfileutils import ls as old_ls

from PyXSE import ls
from PyXSE.Table import Table
from PyXSE.TableSchema import TableSchema
from PyXSE.DatabaseSchema import DatabaseSchema

class TableNotFoundError(Exception):
    def __init__(self, name):
        Exception.__init__(self, "Table [%s] not found" % (name))

class DuplicateTableError(Exception):
    def __init__(self, name):
        es = "Found duplicate tables with name [%s]"
        Exception.__init__(self, es % (name))

class Database:

    """Database represents a collection of Tables."""

    ###
    # TODO: Add checks to get_table
    #       Add Errors like 'TableNotFoundError', 'DuplicateTableError', etc.
    ###

    def __init__(self, folder):
        """Initializes a Database.
        
        folder: a string representing the folder in which to create the
                database.  If it already exists, the current database is
                loaded.  If it does not exist, it is created.

        For example
        
        >>> db = Database('/var/lib/cblogentries')
        >>> db.name
        'cblogentries'
        >>> db.folder
        '/var/lib/cblogentries'
        >>> db.schema.filename
        '/var/lib/cblogentries/db_schema.xml'
        
        """
        if not os.path.isdir(folder):
            create_folder(folder)
        self.folder = folder
        self.schema = DatabaseSchema(self.folder)

    def __list_tables(self):
        """Returns a list of table names."""
        return [x for x in old_ls(self.folder) if x != self.schema.filename]

    def get_table(self, name):
        """Returns the Table with name 'name'.

        name: a string representing the name of the Table to retrieve.

        """
        out = [t for t in self.get_tables() if t.name == name]
        nt = len(out)
        if nt == 0:
            raise TableNotFoundError(name)
        if nt > 1:
            raise DuplicateTableError(name)
        return out[0]

    def get_tables(self):
        """Returns a list of Table objects."""
        return [Table(x) for x in self.__list_tables()]

    def create_table(self, table_name, column_names, column_types, unique=[]):
        """Creates a new Table.

        table_name:   a string representing the table's name.  This must
                      be alphanumeric and unique in this database.
        column_names: a sequence of strings representing the names of
                      the table's columns.
        column_types: a sequence of strings representing the types of
                      the table's columns.
        unique:       a sequence of strings representing the names of
                      the columns which are to be unique.

        """
        try:
            if self.get_table(table_name) is not None:
                raise Exception, "Table %s already exists" % (table_name)
        except TableNotFoundError: # This is actually what we want
            pass
        number_of_names =  len(column_names)
        number_of_types = len(column_types)
        if number_of_names > number_of_types:
            es = "Got %s names, but only %s types"
            raise Exception, es % (number_of_names, number_of_types)
        if number_of_names < number_of_types:
            es = "Got %s types, but only %s names"
            raise Exception, es % (number_of_types, number_of_names)
        for x in unique:
            if x not in column_names:
                raise Exception("Undefined column %s" % (x))
        table_folder = os.path.join(self.folder, table_name)
        create_folder(table_folder)
        schema = TableSchema(table_folder)
        for column_name, column_type in zip(column_names, column_types):
            is_unique = column_name in unique
            schema.add_column(column_name, column_type, unique=is_unique)

    def rename_table(self, table_name, new_name):
        """Renames a table.

        table_name:   a string representing the table's name.  This must
                      be alphanumeric and unique in this database.
        new_name:     a string representing the new name to assign to
                      the table.


        """
        raise NotImplementedError()

    def drop_table(self, table_name):
        """Drops a Table from this database.

        table_name: the name of the Table to drop.

        """
        table = self.get_table(table_name)
        if not table:
            raise TableNotFoundError(table_name)
        rmtree(self.get_table(table_name).folder)

    def select(self, table_name,
                     where=[],
                     sort_key=False,
                     reverse=False,
                     limit=False):
        """Returns a tuple of Row objects.

        table_name: name of the table to select from.
        where:      a sequence of callables that expect a Row object as
                    their only argument.
        sort_key:   optional, a string representing an attribute to sort
                    by.  If this is not in Row.supported_properties, an
                    Exception will be raised.
        reverse:    optional, a boolean.  If True and sort_key is given,
                    the results are reversed.
        limit:      optional, an integer. If given and greater than
                    zero, only this number of results or fewer are
                    returned.

        """
        table = self.get_table(table_name)
        return table.select(where, sort_key, reverse, limit)

    def insert(self, table_name, values):
        """Inserts values into a Table.

        table_name:   a string representing the name of the table in
                      which to insert data.
        values:       a sequence of values representing values to
                      insert into the table.  These must map directly
                      with 'column_names'

        """
        table = self.get_table(table_name)
        return table.insert(values)

    def delete(self, table_name, where=[]):
        """Deletes rows from a table.

        table_name: name of the table to select from.
        where:      a sequence of callables that expect a Row object as
                    their only argument.

        """
        table = self.get_table(table_name)
        return table.delete(where)

    def update(self, table_name, set, where=[]):
        """Updates rows in a table.

        table_name:   name of the table to select from.
        set:          a sequence of callables that expect a Row object
                      as their only argument.
        where:        a sequence of callables that expect a Row object
                      as their only argument.

        """
        table = self.get_table(table_name)
        return table.update(where, set)
